記得我們在 ref 的篇章有講過「解包」嗎?
官方文件在帶過 reactive 後,又細講了它倆解包的細節⋯⋯小菜菜在學習這邊的時候遇到了幾個蠻有趣的狀況坑,來跟大家分享一下。![]()
解包,也就是 unpacking,在程式的世界中指的是:將物件、陣列這種複合的數據中的屬性提取出來。
(可能在不同語言中不太一樣,這邊就是講解 Vue 的喔)
Vue 中的解包概念,如 JS 原生的「解構賦值」,可以將 {}、[] 中的value 直接用簡化的方式存取到變數中。
// 宣告一個物件
const obj = { a: 1, b: 2 };
// 未解構範例
console.log(obj.a, obj.b); // 使用一般「物件.屬性」的方式,印出 1 2
// 解構範例
const { a, b } = obj; // 解構賦值
console.log(a, b); // 使用直接「存取解構後的變數」的方式,印出 1 2
(而這邊就不對原生 JS 解構技巧多作講述,大家可以到 MDN 了解)
直接來看 Vue 響應式用法 ref、reactive 中解包的稀奇古怪狀況!
如在 ref 上篇的範例二 中測試後的結論:
<script setup> import { ref } from "vue"; const count = ref(0); </script> <template> <button @click="count++">{{ count }}</button> </template>在 中,Vue 會自動解包 ref 的物件,所以不需要寫成 count.value,可以直接透過 count 去取值。
count++、{{ count }} 都是解包後的寫法喔!
ref() 其中包裹一個原始值,在模板中是會解包的。
如 ref 下篇,在「深層響應性中的範例」 中測試後的結論:ref() 其中包裹一個物件,在模板中是不會解包的。
以下程式碼,count、obj 為頂級屬性,在模板使用會被解包。obj.countKey 不是頂級屬性,在模板使用不會被解包。
const count = ref(0);
const obj = { countKey: ref(1) };
依循步驟試試看它們都是些什麼!
先印出兩者的 RefImpl 物件:

而如果我們在模板這樣使用:
<template>
<h3>count:{{ count }}</h3>
<h3>count+1:{{ count + 1 }}</h3>
<h3>obj.countKey:{{ obj.countKey }}</h3>
<h3>obj.countKey+1:{{ obj.countKey + 1 }}</h3>
</template>
分別會印出:
來解析一下:
{{ count }}:為頂級屬性,會被解包。{{ count + 1 }}:正常的反應了響應性。{{ obj.countKey }}:非頂級屬性,不會被解包,但這邊卻有了奇怪的現象,為什麼還是印出 1 呢。這裡是官方文件提到的:
另一個需要注意的點是,如果 ref 是文本插值的最終計算值 (即 {{ }} 標籤),那麼它將被解包:
該特性僅僅是文本插值的一個便利特性,等價於 {{ object.id.value }}。
會被解包,是因為 {{ obj.countKey }} 是 ref(1),是文本插值的最終計算值。
{{ obj.countKey + 1 }}:印出了 [object Object]1。[object Object]是 JS 中,將物件強行轉為字串型別時,會呈現的字串。obj.countKey + 1 程式碼中:obj.countKey 並非被解包的數字,而是 RefImpl 物件。我們可以透過解構,將其定義為頂級屬性:
const obj = { countKey: ref(1) };
const { countKey } = obj;
我們將其印出:
<template>
<h3>解構的 countKey:{{ countKey }}</h3>
<h3>解構的 countKey+1:{{ countKey + 1 }}</h3>
</template>
就會是預期中的結果啦:countKey 是在模板中被解包了的 ref(1),即 1。countKey+1 為 2。
而在這邊我們先提一下官方文件在 章節 額外的 ref 解包細節 提到的要點:
作為 reactive 對象的屬性
一個 ref 會在作為響應式對象的屬性被訪問或修改時自動解包。換句話說,它的行為就像一個普通的屬性
看英文版的比較清楚:
A ref is automatically unwrapped when accessed or mutated as a property of a reactive object. In other words, it behaves like a normal property
“as a property of a reactive object.” 也就是:當 ref 被 reactive 綁定的時候。
因此,解包的行為讓 ref 這個 RefImpl 物件「像一般的物件屬性」一樣,不用再 .value 處理內部的值。
請銘記「像一般的物件屬性」這個感受!
<script setup>
import { ref } from "vue";
import { reactive } from "vue";
const countRef = ref(0);
const reactiveObj = reactive({ count: countRef });
console.log(reactiveObj);
console.log(reactiveObj.count);
</script>
<template>
<h3>reactiveObj:{{ reactiveObj }}</h3>
<h3>用 .count 存取內部屬性值:{{ reactiveObj.count }}</h3>
</template>
讓我們一步一步看看以上做了什麼:
ref(0) 這個 RefImpl 物件存到 countRef 變數中。const countRef = ref(0);
countRef 變數定義為 count 屬性的值,並以物件包起來,當作 reactive 的參數,再存進 reactiveObj 變數。const reactiveObj = reactive({ count: countRef });
reactiveObj 和 reactiveObj.count 的結果console.log(reactiveObj);
console.log(reactiveObj.count);
reactiveObj 和 reactiveObj.count 的值<template>
<h3>reactiveObj:{{ reactiveObj }}</h3>
<h3>用 .count 存取內部屬性值:{{ reactiveObj.count }}</h3>
</template>
瀏覽器上會呈現什麼呢?
右邊 console.log:
console.log(reactiveObj);:reactiveObj 為我們用 reactive 綁定的 { count: countRef },是一個 Proxy 物件。console.log(reactiveObj.count);:這裡發生了解包。reactiveObj 用 .count 存取屬性的方式,直接存取到了其中 ref 物件內的 _value。左邊畫面:
reactiveObj 為我們綁定的 { count: countRef } 物件。countRef 自動解包了 ref(0),因此呈現 { "count": 0 }。reactiveObj.count:這裡發生了解包。reactiveObj 用 .count 存取屬性的方式,直接存取到了其中 ref 物件內的 _value。因此,當 ref 成為了 reactive() 物件中的「屬性」時,會讓 ref 物件在 reactive 中可以「像一般的物件屬性」被存取。
ref 若以「陣列型態」存在 reactive 中的話不會解包。
官方文件:與 reactive 對象不同的是,當 ref 作為響應式數組或原生集合類型 (如 Map) 中的元素被訪問時,它不會被解包:
const books = reactive([ref('Vue 3 Guide')]) // 這裡需要 .value console.log(books[0].value)
我們可以試試看用 reactive 包一個 ref 的陣列。
const classmates = reactive([ref("Jami"), ref("Irene"), ref("Jenny")]);
console.log(classmates);
印出來看~reactive 物件中有一個陣列,陣列其中有三個 RefImpl 物件:
console.log(classmates[0]);
我們取得第一個陣列,印出來看看:
這情況不會被解包。
(和 ref 以「物件」形式傳入 reactive 的時候的解包情況不同)
因此我若以 _value 語法,就可以取到內部的 Jami 囉。
console.log(classmates[0].value);

另外我們試試看用 reactive 包一個 ref 的 Map。
const classmateMap = new Map([["name", ref("Jami")]])
console.log(classmateMap.get(name))
印出來是一個 RefImpl 物件,並未解包:
需要用 _value 方式取得值:
console.log(classmateMap.get("name").value);

這一小節是我自己在實驗的時候,發現的情況,問問了我的前端捧友⋯⋯他說實務上很少遇到,但我就也是確實在學習的過程中踩到了 QQ 陪我研究一下吧(在跟誰講話)
如果我的 ref 物件不包起來當作 reactive 的屬性,而是「直接傳入」呢?
我們實作一下範例,把 ref 分別用兩種形式傳給 reactive:
<script setup>
import { ref } from "vue";
import { reactive } from "vue";
const count = ref(0);
const reactiveObj = reactive({ count }); // 作為物件傳入
const reactiveObj2 = reactive(count); // 直接傳入
console.log(`reactiveObj`, reactiveObj);
console.log(`reactiveObj.count`, reactiveObj.count);
console.log(`reactiveObj2`, reactiveObj2);
console.log(`reactiveObj2.value`, reactiveObj2.value);
</script>
瀏覽器上結果:
reactiveObj 是 ref 作為「物件」傳入 reactive 的方式得出的。reactiveObj2:是 ref 「直接」傳入 reactive 的方式得出的。來看看都印出了什麼東東:
reactiveObj:結果為一個 Proxy 物件,內部有屬性 count,值為 ref(0) 這個 RefImpl 物件。reactiveObj.count:以 .count 取 reactiveObj 的值,為 0(其中 ref(0) 發生解包,所以不用再 .value )。reactiveObj2:結果為一個 Proxy 物件,內部直接是 ref(0) 這個 RefImpl 物件。reactiveObj2.value:由於內部是一個 RefImpl 物件,並非 Proxy 響應式物件,需用 _value 取值,為 0(不會被解包)。我們加上這段:
<template>
<h3>reactiveObj.count >> {{ reactiveObj.count }}</h3>
<h3>reactiveObj2.value >> {{ reactiveObj2.value }}</h3>
<h3>reactiveObj2 >> {{ reactiveObj2 }}</h3>
</template>
再到瀏覽器上看看:
1.reactiveObj.count:以 .count 取 reactiveObj 的值,為 0(其中 ref(0) 發生解包,所以不用再 .value ),這沒問題。
再來是不是發現了什麼詭異的情況?
reactiveObj2.value:reactiveObj2 是 ref 「直接」傳入 reactive 得出的。console.log 就是這樣取值,為什麼到這邊卻沒有印出東西?reactiveObj2 又印出 0?停下來,思考一下這幾點:
ref(0) 在模板會自動解包,直接變成 0。reactive() 只會接受「物件」屬性作為參數。因此 Vue 會嘗試將解包後的 0 像 reactive(0) 這樣處理。
重新看一下程式碼:
<script setup>
const count = ref(0);
const reactiveObj2 = reactive(count); // 直接傳入
</script>
<template>
<h3>reactiveObj2.value >> {{ reactiveObj2.value }}</h3>
<h3>reactiveObj2 >> {{ reactiveObj2 }}</h3>
</template>
回答問題!
reactiveObj2.value:reactiveObj2 是 ref 「直接」傳入 reactive 得出的。console.log 就是這樣取值,為什麼到這邊卻沒有印出東西?reactive() 只會接受「物件」屬性作為參數,而 0 並不是一個物件,它沒有 .value 屬性,因此顯示不出來。reactiveObj2 又印出 0?reactive() 只會接受「物件」屬性作為參數,但如果傳入的是一個非物件類型,還是會正常呈現,這邊我想到的是上面說的 ref 若是文本插值的最終值的話,就會被解包。這暫時還沒有找到文件論點支持這個想法
目前的想法是覺得:Vue 沒有報錯,是反映了ref(0)解包後的真實情況。
實際上就是?
<template>
<h3>reactive(0) >> {{ reactive(0) }}</h3>
</template>

整理為一個表格,不然好像有點累:
| 情境 | 解包行為 |
|---|---|
| 模板中的 ref 為原始值 | 會解包 |
| 模板中的 ref 為物件值 | 不解包 |
| 模板中的 ref 為頂級屬性 | 會解包 |
| ref 以 {} 物件型態,作為 reactive 參數 | 會解包 |
| ref 以陣列型態,作為 reactive 參數 | 不解包 |
| ref(RefImpl 物件)直接作為 reactive 參數 | 不解包 |
感謝看到這裡的你⋯⋯相信到這邊大家是否也鬆一口氣了。
深呼吸一下⋯⋯
響應式基礎的內容我們到這邊告一個段落,為大家附上小目錄 標題也太長了吧:
這篇寫到有點超出預期的發展(字數和資訊量都是 XD),也有點不太確定自己吸收的和實證的是否正確,或是有更完善的思考方式和範例,歡迎大家提點!
鐵人賽經過 1/2 啦~~~~~~~![]()
https://github.com/Jamixcs/2024iThome-jamixcs/tree/main/src/components/day15